package com.hqd.ch03.v49.context.annotation;

import com.hqd.ch03.utils.ClassUtils;
import com.hqd.ch03.utils.ObjectUtils;
import com.hqd.ch03.utils.ReflectionUtils;
import com.hqd.ch03.v49.aopalliance.intercept.MethodInterceptor;
import com.hqd.ch03.v49.aopalliance.intercept.MethodInvocation;
import com.hqd.ch03.v49.aware.BeanFactoryAware;
import com.hqd.ch03.v49.factory.BeanFactory;
import com.hqd.ch03.v49.factory.ConfigurableBeanFactory;
import net.sf.cglib.proxy.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;

public class ConfigurationClassEnhancer {

    // The callbacks to use. Note that these callbacks must be stateless.
    private static final Callback[] CALLBACKS = new Callback[]{
            new BeanMethodInterceptor(),
            new BeanFactoryAwareMethodInterceptor(),
            NoOp.INSTANCE
    };

    private static final ConditionalCallbackFilter CALLBACK_FILTER = new ConditionalCallbackFilter(CALLBACKS);

    private static final String BEAN_FACTORY_FIELD = "$$beanFactory";


    private static final Log logger = LogFactory.getLog(ConfigurationClassEnhancer.class);


    /**
     * Loads the specified class and generates a CGLIB subclass of it equipped with
     * container-aware callbacks capable of respecting scoping and other bean semantics.
     *
     * @return the enhanced subclass
     */
    public Class<?> enhance(Class<?> configClass, ClassLoader classLoader) {
        if (EnhancedConfiguration.class.isAssignableFrom(configClass)) {
            if (logger.isDebugEnabled()) {
                logger.debug(String.format("Ignoring request to enhance %s as it has " +
                                "already been enhanced. This usually indicates that more than one " +
                                "ConfigurationClassPostProcessor has been registered (e.g. via " +
                                "<context:annotation-config>). This is harmless, but you may " +
                                "want check your configuration and remove one CCPP if possible",
                        configClass.getName()));
            }
            return configClass;
        }
        Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
        if (logger.isTraceEnabled()) {
            logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
                    configClass.getName(), enhancedClass.getName()));
        }
        return enhancedClass;
    }

    /**
     * Creates a new CGLIB {@link Enhancer} instance.
     */
    private Enhancer newEnhancer(Class<?> configSuperClass, ClassLoader classLoader) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(configSuperClass);
        enhancer.setInterfaces(new Class<?>[]{EnhancedConfiguration.class});
        enhancer.setUseFactory(false);
        enhancer.setCallbackFilter(CALLBACK_FILTER);
        enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
        return enhancer;
    }

    /**
     * Uses enhancer to generate a subclass of superclass,
     * ensuring that callbacks are registered for the new subclass.
     */
    private Class<?> createClass(Enhancer enhancer) {
        Class<?> subclass = enhancer.createClass();
        // Registering callbacks statically (as opposed to thread-local)
        // is critical for usage in an OSGi environment (SPR-5932)...
        Enhancer.registerStaticCallbacks(subclass, CALLBACKS);
        return subclass;
    }


    /**
     * Marker interface to be implemented by all @Configuration CGLIB subclasses.
     * Facilitates idempotent behavior for {@link ConfigurationClassEnhancer#enhance}
     * through checking to see if candidate classes are already assignable to it, e.g.
     * have already been enhanced.
     * <p>Also extends {@link BeanFactoryAware}, as all enhanced {@code @Configuration}
     * classes require access to the {@link BeanFactory} that created them.
     * <p>Note that this interface is intended for framework-internal use only, however
     * must remain public in order to allow access to subclasses generated from other
     * packages (i.e. user code).
     */
    public interface EnhancedConfiguration extends BeanFactoryAware {
    }


    /**
     * Conditional {@link Callback}.
     *
     * @see ConditionalCallbackFilter
     */
    private interface ConditionalCallback extends Callback {

        boolean isMatch(Method candidateMethod);
    }


    /**
     * A {@link CallbackFilter} that works by interrogating {@link Callback Callbacks} in the order
     * that they are defined via {@link ConditionalCallback}.
     */
    private static class ConditionalCallbackFilter implements CallbackFilter {

        private final Callback[] callbacks;

        private final Class<?>[] callbackTypes;

        public ConditionalCallbackFilter(Callback[] callbacks) {
            this.callbacks = callbacks;
            this.callbackTypes = new Class<?>[callbacks.length];
            for (int i = 0; i < callbacks.length; i++) {
                this.callbackTypes[i] = callbacks[i].getClass();
            }
        }

        @Override
        public int accept(Method method) {
            for (int i = 0; i < this.callbacks.length; i++) {
                Callback callback = this.callbacks[i];
                if (!(callback instanceof ConditionalCallback) || ((ConditionalCallback) callback).isMatch(method)) {
                    return i;
                }
            }
            throw new IllegalStateException("No callback available for method " + method.getName());
        }

        public Class<?>[] getCallbackTypes() {
            return this.callbackTypes;
        }
    }


    /**
     * Custom extension of CGLIB's DefaultGeneratorStrategy, introducing a {@link BeanFactory} field.
     * Also exposes the application ClassLoader as thread context ClassLoader for the time of
     * class generation (in order for ASM to pick it up when doing common superclass resolution).
     */


    /**
     * Intercepts the invocation of any {@link BeanFactoryAware#setBeanFactory(BeanFactory)} on
     * {@code @Configuration} class instances for the purpose of recording the {@link BeanFactory}.
     *
     * @see EnhancedConfiguration
     */
    private static class BeanFactoryAwareMethodInterceptor implements MethodInterceptor, ConditionalCallback {

        public static boolean isSetBeanFactory(Method candidateMethod) {
            return (candidateMethod.getName().equals("setBeanFactory") &&
                    candidateMethod.getParameterCount() == 1 &&
                    BeanFactory.class == candidateMethod.getParameterTypes()[0] &&
                    BeanFactoryAware.class.isAssignableFrom(candidateMethod.getDeclaringClass()));
        }

        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            Field field = ReflectionUtils.findField(obj.getClass(), BEAN_FACTORY_FIELD);
            field.set(obj, args[0]);

            // Does the actual (non-CGLIB) superclass implement BeanFactoryAware?
            // If so, call its setBeanFactory() method. If not, just exit.
            if (BeanFactoryAware.class.isAssignableFrom(ClassUtils.getUserClass(obj.getClass().getSuperclass()))) {
                return proxy.invokeSuper(obj, args);
            }
            return null;
        }

        @Override
        public boolean isMatch(Method candidateMethod) {
            return isSetBeanFactory(candidateMethod);
        }

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            return invocation.proceed();
        }
    }


    /**
     * Intercepts the invocation of any {@link Bean}-annotated methods in order to ensure proper
     * handling of bean semantics such as scoping and AOP proxying.
     *
     * @see Bean
     * @see ConfigurationClassEnhancer
     */
    private static class BeanMethodInterceptor implements MethodInterceptor, ConditionalCallback {

        /**
         * Enhance a {@link Bean @Bean} method to check the supplied BeanFactory for the
         * existence of this bean object.
         *
         * @throws Throwable as a catch-all for any exception that may be thrown when invoking the
         *                   super implementation of the proxied method i.e., the actual {@code @Bean} method
         */
        public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs,
                                MethodProxy cglibMethodProxy) throws Throwable {

            ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
            String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);

            // To handle the case of an inter-bean method reference, we must explicitly check the
            // container for already cached instances.

            // First, check to see if the requested bean is a FactoryBean. If so, create a subclass
            // proxy that intercepts calls to getObject() and returns any cached bean instance.
            // This ensures that the semantics of calling a FactoryBean from within @Bean methods
            // is the same as that of referring to a FactoryBean within XML. See SPR-6602.
            if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) &&
                    factoryContainsBean(beanFactory, beanName)) {
                Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName);
                // It is a candidate FactoryBean - go ahead with enhancement
                return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName);
            }


            return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
        }

        private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs,
                                            ConfigurableBeanFactory beanFactory, String beanName) {

            // The user (i.e. not the factory) is requesting this bean through a call to
            // the bean method, direct or indirect. The bean may have already been marked
            // as 'in creation' in certain autowiring scenarios; if so, temporarily set
            // the in-creation status to false in order to avoid an exception.
            boolean useArgs = !ObjectUtils.isEmpty(beanMethodArgs);
            if (useArgs && beanFactory.isSingleton(beanName)) {
                // Stubbed null arguments just for reference purposes,
                // expecting them to be autowired for regular singleton references?
                // A safe assumption since @Bean singleton arguments cannot be optional...
                for (Object arg : beanMethodArgs) {
                    if (arg == null) {
                        useArgs = false;
                        break;
                    }
                }
            }
            Object beanInstance = (useArgs ? beanFactory.getBean(beanName) :
                    beanFactory.getBean(beanName));
            if (!ClassUtils.isAssignableValue(beanMethod.getReturnType(), beanInstance)) {
                // Detect package-protected NullBean instance through equals(null) check
                if (beanInstance.equals(null)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(String.format("@Bean method %s.%s called as bean reference " +
                                        "for type [%s] returned null bean; resolving to null value.",
                                beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(),
                                beanMethod.getReturnType().getName()));
                    }
                    beanInstance = null;
                }
            }
            return beanInstance;
        }

        @Override
        public boolean isMatch(Method candidateMethod) {
            return (candidateMethod.getDeclaringClass() != Object.class &&
                    !BeanFactoryAwareMethodInterceptor.isSetBeanFactory(candidateMethod) &&
                    BeanAnnotationHelper.isBeanAnnotated(candidateMethod));
        }

        private ConfigurableBeanFactory getBeanFactory(Object enhancedConfigInstance) {
            Field field = ReflectionUtils.findField(enhancedConfigInstance.getClass(), BEAN_FACTORY_FIELD);
            Object beanFactory = ReflectionUtils.getField(field, enhancedConfigInstance);
            return (ConfigurableBeanFactory) beanFactory;
        }

        private boolean factoryContainsBean(ConfigurableBeanFactory beanFactory, String beanName) {
            return beanFactory.containsBean(beanName);
        }


        /**
         * Create a subclass proxy that intercepts calls to getObject(), delegating to the current BeanFactory
         * instead of creating a new instance. These proxies are created only when calling a FactoryBean from
         * within a Bean method, allowing for proper scoping semantics even when working against the FactoryBean
         * instance directly. If a FactoryBean instance is fetched through the container via &-dereferencing,
         * it will not be proxied. This too is aligned with the way XML configuration works.
         */
        private Object enhanceFactoryBean(Object factoryBean, Class<?> exposedType,
                                          ConfigurableBeanFactory beanFactory, String beanName) {

            try {
                Class<?> clazz = factoryBean.getClass();
                boolean finalClass = Modifier.isFinal(clazz.getModifiers());
                boolean finalMethod = Modifier.isFinal(clazz.getMethod("getObject").getModifiers());
                if (finalClass || finalMethod) {
                    if (exposedType.isInterface()) {
                        if (logger.isTraceEnabled()) {
                            logger.trace("Creating interface proxy for FactoryBean '" + beanName + "' of type [" +
                                    clazz.getName() + "] for use within another @Bean method because its " +
                                    (finalClass ? "implementation class" : "getObject() method") +
                                    " is final: Otherwise a getObject() call would not be routed to the factory.");
                        }
                        return createInterfaceProxyForFactoryBean(factoryBean, exposedType, beanFactory, beanName);
                    } else {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Unable to proxy FactoryBean '" + beanName + "' of type [" +
                                    clazz.getName() + "] for use within another @Bean method because its " +
                                    (finalClass ? "implementation class" : "getObject() method") +
                                    " is final: A getObject() call will NOT be routed to the factory. " +
                                    "Consider declaring the return type as a FactoryBean interface.");
                        }
                        return factoryBean;
                    }
                }
            } catch (NoSuchMethodException ex) {
                // No getObject() method -> shouldn't happen, but as long as nobody is trying to call it...
            }

            return createCglibProxyForFactoryBean(factoryBean, beanFactory, beanName);
        }

        private Object createInterfaceProxyForFactoryBean(Object factoryBean, Class<?> interfaceType,
                                                          ConfigurableBeanFactory beanFactory, String beanName) {

            return Proxy.newProxyInstance(
                    factoryBean.getClass().getClassLoader(), new Class<?>[]{interfaceType},
                    (proxy, method, args) -> {
                        if (method.getName().equals("getObject") && args == null) {
                            return beanFactory.getBean(beanName);
                        }
                        return ReflectionUtils.invokeMethod(method, factoryBean, args);
                    });
        }

        private Object createCglibProxyForFactoryBean(Object factoryBean,
                                                      ConfigurableBeanFactory beanFactory, String beanName) {

            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(factoryBean.getClass());
            enhancer.setCallbackType(MethodInterceptor.class);

            // Ideally create enhanced FactoryBean proxy without constructor side effects,
            // analogous to AOP proxy creation in ObjenesisCglibAopProxy...
            Class<?> fbClass = enhancer.createClass();
            Object fbProxy = null;


            if (fbProxy == null) {
                try {
                    fbProxy = ReflectionUtils.accessibleConstructor(fbClass).newInstance();
                } catch (Throwable ex) {
                    throw new IllegalStateException("Unable to instantiate enhanced FactoryBean using Objenesis, " +
                            "and regular FactoryBean instantiation via default constructor fails as well", ex);
                }
            }


            return fbProxy;
        }

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            return invocation.proceed();
        }
    }

}
